from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
from shared_enum import Layer, Process
from frame_structure import Frame, Ontology, ElementContainer, ListContainer, SetContainer, BaseContainer, SingleContainer
from visual_structure import VisualStructure

if TYPE_CHECKING:
    from frame_manager import FrameManager

T = TypeVar('T')

#region decorators

# must pass frame object first!
def frame_process_restrain(stage_at_least: Process, stage_at_most: Process = Process.Stored):
    def decorator(fun: Callable[..., T]) -> Callable[..., T]:
        def wrapper(*args, **kwargs):
            if isinstance(args[0], Frame):
                frame = args[0]
                if (frame._process >= stage_at_least) and (frame._process <= stage_at_most):
                    return fun(*args, **kwargs)
                else:
                    raise Exception('frame process stage error')
            else:
                raise Exception('must pass frame object as first argument when use process restrain')
        return wrapper
    return decorator

def return_frame_if_exists(layer: Layer):
    def decorator(fun: Callable[[Frame], Frame]) -> Callable[[Frame], Frame]:
        def wrapper(frame: Frame) -> Frame:
            if layer is Layer.Annotation:
                marker_map = frame.frame_manager.marker_map
            elif layer is Layer.Primary:
                marker_map = frame.frame_manager.marker_map_primary
            elif layer is Layer.Prototype:
                marker_map = frame.frame_manager.marker_map_prototype
            else:
                raise ValueError
            if frame.uuid in marker_map:
                return marker_map[frame.uuid]
            else:
                return fun(frame)
        return wrapper
    return decorator

def only_frame_do_for_apply_decorator(fun: Callable[[T], T]) -> Callable[[T], T]:
    def wrapper(value: T) -> T:
        if isinstance(value, Frame):
            return fun(value)
        else:
            return value
    return wrapper

#endregion

#region apply_frame_script

def filled_frame_apply_script(script_name: str, **kwargs) -> Callable[[T], T]:
    @only_frame_do_for_apply_decorator
    @frame_process_restrain(Process.Filled)
    def get_script(frame: Frame) -> Frame:
        return frame.script[script_name](frame, **kwargs)
    return get_script

def checked_frame_apply_script(script_name: str, **kwargs) -> Callable[[T], T]:
    @only_frame_do_for_apply_decorator
    @frame_process_restrain(Process.Checked)
    def get_script(frame: Frame) -> Frame:
        return frame.script[script_name](frame, **kwargs)
    return get_script

#endregion

#region tools
def copy_meta_data_to_new_frame(from_frame: Frame, to_frame: Frame) -> None:
    to_frame.uuid = from_frame.uuid
    to_frame.corpus = from_frame.corpus
    to_frame.source = from_frame.source
    to_frame.elements = {}

#use carefully!
def copy_value_to_new_element_container(from_element: ElementContainer, to_element: ElementContainer) -> None:
    to_element.is_focus = from_element.is_focus
    to_element.value = from_element.value
    to_element._process = from_element._process
    if isinstance(to_element, ListContainer) or isinstance(to_element, SetContainer):
        to_element.focus_items = from_element.focus_items

def if_only_one_ontology_exist_in_frame(frame: Frame) -> tuple[bool, Ontology]:
    counter = 0
    for element_name, element in frame.elements.items():
        if isinstance(element, BaseContainer):
            return False, None
        elif isinstance(element, SingleContainer):
            if isinstance(element.value, Ontology):
                counter += 1
                ontology = element.value
            else:
                return False, None
        else:
            if len(element.value) > 1:
                return False, None
            for value in element.value:
                if isinstance(value, Ontology):
                    counter += 1
                    ontology = value
                else:
                    return False, None
    if counter == 1:
        return True, ontology 
    else:
        return False, None

def get_string_from_structures(value) -> str:
    if isinstance(value, Ontology):
        return value.name
    elif isinstance(value, Frame):
        return value.script['visual'](value).to_string()


def get_string_from_element(frame: Frame, element_name: str, splitmark: str = ','):
    if element_name in frame.elements:
        element_container = frame.elements[element_name]
        if isinstance(element_container, BaseContainer):
            return str(element_container.value)
        elif isinstance(element_container, SingleContainer):
            return get_string_from_structures(element_container.value)
        else:
            _str_list: list[str] = []
            for value in element_container.value:
                _str_list.append(get_string_from_structures(value))
            return splitmark.join(_str_list)
    else:
        return ''

#endregion

#region common_funs
def common_filter_for_apply(is_core: bool, is_focus: bool) -> bool:
    return is_core or is_focus

@return_frame_if_exists(Layer.Primary)
def common_to_primary(value: Frame) -> Frame:
    frame_manager = value.frame_manager
    _frame = Frame(frame_manager, value.name)
    copy_meta_data_to_new_frame(value, _frame)
    _frame.layer = Layer.Primary
    for element_name, element in value.elements.items():
        _element = element.apply(checked_frame_apply_script('primary'))
        _frame.elements[element_name] = _element
    _frame._process = Process.Checked
    frame_manager.marker_map_primary[_frame.uuid] = _frame
    return _frame

@return_frame_if_exists(Layer.Prototype)
def common_to_prototype(value: Frame) -> Frame:
    frame_manager = value.frame_manager
    _frame = Frame(frame_manager, value.name)
    copy_meta_data_to_new_frame(value, _frame)
    _frame.layer = Layer.Prototype
    for element_name, element in value.elements.items():
        _element = element.apply(checked_frame_apply_script('prototype'), common_filter_for_apply)
        if _element is not None:
            _frame.elements[element_name] = _element
    _frame._process = Process.Checked
    frame_manager.marker_map_prototype[_frame.uuid] = _frame
    return _frame

def common_generate(value: Frame) -> Frame:
    for element_name, element in value.elements.items():
        fun = filled_frame_apply_script('generate')
        value.elements[element_name] = element.apply(fun)
    return value

def common_check(value: Frame) -> bool:
    for element_name, element in value.elements.items():
        if isinstance(element, SingleContainer):
            _element = element.value
            if isinstance(_element, Frame):
                _element.check(_element.script['check'], _element.script['generate'])
        elif isinstance(element, ListContainer) or isinstance(element, SetContainer):
            for _element in element.value:
                if isinstance(_element, Frame):
                    _element.check(_element.script['check'], _element.script['generate'])
    return True

def not_implement(*args, **kwargs):
    raise NotImplementedError

def common_to_prototype_allow_to_ontology(value: Frame) -> Union[Frame, Ontology]:
    _frame = common_to_prototype(value)
    res = if_only_one_ontology_exist_in_frame(_frame)
    if res[0]:
        _frame.frame_manager.marker_map_prototype[_frame.uuid] = res[1]
        return res[1]
    else:
        return _frame

#endregion